El objetivo de este proyecto es poder elaborar un modelo que nos permita identificar la potencial rentabilidad de un nuevo video juego, de manera de determinar si vale la pena o no invertir en él. Para esto, el experto indica que esta potencial rentabilidad está relacionada con la posible evaluación que le den los usuario al juego y con las ventas estimadas de éste. Para poder llevar acabo este modelo, se nos facilita un conjunto de datos con 7881 registros y 16 atributos, en donde cada registro corresponde a un video juego, y los 16 atributos son características de estos juegos, en donde destacan la empresa que los lanzó, la empresa que los desarrolló, la fecha de lanzamiento, el o los géneros asociados al juego, las ventas estimadas, la evaluación de los usuarios, entre otros atributos. Dentro de este conjunto de datos, la variable ventas estimadas (estimated_sells), y evaluación de los usuarios (rating) son nuestras variables objetivos o a predecir, y todas las demás serán las variables predictoras.
El problema se dividirá en dos partes, en la primera se buscará predecir el rating que tenga el juego en base a sus características, y en la segunda se buscará predecir las estimated_sells. La primera parte se evalua respecto a la métrica f1_weighted, ya que la idea es equilibrar qué tan bien anda el modelo tanto con su precision como con su recall, teniendo en consideración la cantidad de ejemplos por cada clase. Por otro lado, la segunda parte se evaluará según r^2, que representa qué tanta variabilidad del problema es explicada por el modelo.
Para resolver este problema, se realizaron varias transformaciones a los datos de entrada, en donde las variables numéricas se escalaron para dejarlas todas dentro del mismo orden de magnitud y las variables categóricas se transformaron en numéricas a través de distintos mecanismos dependiendo de la naturaleza de la variable categórica, cuyo detalle se explica más adelante. Luego, se estableció un baseline para los dos problemas a resolver, en donde para la clasificación se utilizó un KNN y para la regresión un árbol de decisión, obteniendo resultados bastante deplorables, sobre todo para el problema de regresión. Luego para mejorar los resultados obtenidos en el baseline, se procedió a realizar una búsqueda de grilla variando entre dos modelos de ensamblaje y distintos hiperparámetros a modificar, y los obtenidos lograron superar el baseline, sin ser considerablemente mayores, pero por lo menos se cumplió el objetivo.
Respecto a los resultados obtenidos en comparación con los demás concursantes de la compentencia, podemos decir que hasta el momento en que este informe fue escrito, en el problema de clasificación logré un 9no lugar a 0.058 puntos del primer lugar, y en el problema de regresión un 4to lugar a 0.24 puntos del primer lugar.
En esta sección se realizará el análisis exploratorio de los datos, en donde se realizará un análisis univariado y multivariado. Primero se partirá realizando un pandas profiling que nos entrega un informe bien completo sobre el conjuntos de datos que estaremos trabajando.
#Importar librerías
import pandas as pd
import numpy as np
from pandas_profiling import ProfileReport
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import plotly.express as px
from umap import UMAP
from sklearn.decomposition import PCA
import nltk
from nltk.corpus import stopwords
from nltk import word_tokenize
from nltk.stem import PorterStemmer
nltk.download('stopwords')
nltk.download('punkt')
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.preprocessing import RobustScaler
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import MinMaxScaler
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import FunctionTransformer
from sklearn.naive_bayes import GaussianNB
# Metricas de evaluación
from sklearn.metrics import classification_report
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
from sklearn.metrics import cohen_kappa_score
from sklearn.metrics import confusion_matrix
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.feature_selection import f_classif, mutual_info_classif
#Clasificadores
from sklearn.svm import SVC, LinearSVC
from sklearn.svm import SVR
from sklearn.tree import DecisionTreeClassifier
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.ensemble import GradientBoostingRegressor
[nltk_data] Downloading package stopwords to [nltk_data] C:\Users\stefanosch\AppData\Roaming\nltk_data... [nltk_data] Package stopwords is already up-to-date! [nltk_data] Downloading package punkt to [nltk_data] C:\Users\stefanosch\AppData\Roaming\nltk_data... [nltk_data] Package punkt is already up-to-date!
#Se importa el dataset
df = pd.read_pickle(filepath_or_buffer = 'data/train.pickle')
df.head()
| name | release_date | english | developer | publisher | platforms | required_age | categories | genres | tags | achievements | average_playtime | price | short_description | estimated_sells | rating | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | An Aspie Life | 2018-03-29 | 1 | Bradley Hennessey;Joe Watson | EnderLost Studios | windows | 0 | Single-player;Steam Achievements | Adventure;Casual;Free to Play;Indie;Simulation | Free to Play;Adventure;Indie | 23 | 0 | 0.00 | One day your roommate Leaves for no reason. Yo... | 3914 | Mixed |
| 1 | GhostControl Inc. | 2014-06-06 | 1 | bumblebee | Application Systems Heidelberg | windows;mac;linux | 0 | Single-player;Steam Achievements;Steam Trading... | Casual;Indie;Simulation;Strategy | Turn-Based;Indie;Simulation | 53 | 65 | 10.99 | Manage a team of ghosthunters and free London ... | 10728 | Mixed |
| 2 | Deponia | 2012-08-06 | 1 | Daedalic Entertainment | Daedalic Entertainment | windows;mac;linux | 0 | Single-player;Steam Achievements;Steam Trading... | Adventure;Indie | Adventure;Point & Click;Comedy | 19 | 217 | 6.99 | In Deponia, the world has degenerated into a v... | 635792 | Positive |
| 3 | Atlas Reactor | 2016-10-04 | 1 | Trion Worlds | Trion Worlds | windows | 0 | Multi-player;Online Multi-Player;Steam Achieve... | Free to Play;Strategy | Free to Play;Multiplayer;Strategy | 121 | 1240 | 0.00 | SEASON 6 NOW LIVE! The battle for Atlas contin... | 253864 | Positive |
| 4 | CHUCHEL | 2018-03-07 | 1 | Amanita Design | Amanita Design | windows;mac | 0 | Single-player;Steam Achievements;Steam Trading... | Adventure;Casual;Indie | Adventure;Indie;Casual | 7 | 245 | 7.99 | CHUCHEL is a comedy adventure game from the cr... | 49818 | Mostly Positive |
profile = ProfileReport(df, title="Pandas Profiling Report",)
profile
El reporte que entrega pandas permite observar que el dataset no consta con valores faltante ni tampoco de registros duplicados. Mencionar que consta de 16 variables de las cuales sólo 5 son numéricas, las demás categóricas. Sobre las numéricas notar que todas poseen curvas muy asimétricas, concentrando la mayoría de sus valores en el lado izquierdo de su distribución, haciendo que los valores obtenidos para la skewness sean altos, en donde la variable price es la que posee el valor más bajo de 1.99. Si observamos el valor para la kurtosis, es posible notar que para todas las variables su valor es mayor a 3, lo que indica que es más probable obtener outliers. También destacar que el percentil 95 del atributo required_age es 0, lo que indica que el 95% de los juegos no tiene restricción de edad.
Sobre las variables categóricas mencionar que el nombre no entrega información ya que todos sus valores son únicos, por lo que no se utilizará como variable predictora. El atributo release_date está como categórico, pero debería transformarse a fecha. Por otro lado, notar que el 98.6% de los valores de la variable english son 1, lo que muestra el desbalance de datos sobre esta característica. Destacar que sobre el atributo platforms sólo 1 registro no opera con Windows, los demás todos poseen soporte en este sistema operativo. Por otra parte, las características genres y tags parecieran contener la misma información, con la salvedad de que la primera, es un subconjunto de la segunda.
A continuación se realizará un análisis exploratorio haciendo la distinción sobre la variable a predecir. Primero abordaremos el problema de clasificación, y luego el de regresión.
Para entender cómo se relacionan las demás variables con la variable objetivo para el problema de clasificación, se mostrará cuántas observaciones tenemos para cada valor de esta variable.
df['rating'].value_counts().plot(kind = 'bar')
<AxesSubplot:>
Se puede observar que los datos están desbalanceados, sin embargo la diferencia entre la categoría que posee más observaciones y la que posee menos no es tan extrema, por lo que no debería ser un problema. A continuación se verá cada uno de los atributos de interés cómo se relacionan con nuestra variable objetivo. Las variables name, developer, publisher, categories, genres, tags y short_description, se abordarán al último debido a que como son atributos categóricos con muchos valores únicos, se hace poco práctico su visualización y se deberá manipularlos para poder realiza el análisis explotario.
fig, axes = plt.subplots(3,2, figsize = (20,24))
atributos = list(df.columns.difference(['name', 'estimated_sells',
'rating', 'release_date',
'developer', 'publisher',
'categories', 'genres',
'tags', 'short_description'])) #se excluyen los atributos que no entregan
#información relevante y las variables objetivos
for i in range(3):
for j in range(2):
indice = 2*i + j
if indice <= (len(atributos) - 1):
if df[atributos[indice]].dtypes in ['int64', 'float64']:
sns.histplot(data = df[[atributos[indice], 'rating']],
x = atributos[indice],
hue = 'rating',
ax = axes[i][j],
multiple="stack")
else:
sns.countplot(data = df[[atributos[indice], 'rating']],
x = atributos[indice],
hue = 'rating',
ax = axes[i][j])
axes[i][j].set(xlabel= atributos[indice], ylabel='Frecuencia')
axes[i][j].set_title(atributos[indice])
fig.tight_layout()
Sobre esta visualización, resaltar que la variable english no parece diferenciar entre los distintos valores del rating, ya que cuando el valor es 1, parecieran tener la misma cantidad de registros para cada uno de los valores de rating. Si ahondamos un poco en esta variable podemos observar lo siguiente.
agEnglishRating = df.groupby(by = ['english', 'rating'])['name'].count()
agEnglishRatingPorcentaje = agEnglishRating.groupby(level=0).apply(lambda x: x / float(x.sum()))
agEnglishRatingPorcentaje
english rating
0 Negative 0.080357
Mixed 0.205357
Mostly Positive 0.205357
Positive 0.214286
Very Positive 0.294643
1 Negative 0.164886
Mixed 0.210194
Mostly Positive 0.216759
Positive 0.258334
Very Positive 0.149826
Name: name, dtype: float64
Se aprecia que las proporciones asociadas con rating respecto a si el juego es en inglés o no se mantienen para las categorías Mixed, Mostly Positive y Positive, sin embargo para Negative y Very Positive, que justamente resultan ser las categorías más extremas, los resultados se invierten, en donde cuando el juego no es en inglés, entonces posee menos votos negativos y más votos muy positivos, en cambio cuando es en inglés, recibe el doble (en proporción) de votos negativos y la mitad de votos muy positivos (también respecto a la proporción en su grupo). Notar también que para los juegos que no están en inglés, la categoría que más se vota es Very Positive, mientras que en inglés es la que menos se vota.
Sobre la variable platforms podemos observar que en general para las distintas plataformas las categorías del rating poseen la misma distribución. Llama la atención es que para la plataforma windows exiten más observaciones de rating Mixed, en cambio para todas las demás, la categoría que predomina es Positive. También notar que, en proporción, la plataforma windows es la que posee más votos Negative, superando a la categoría Very Positive, lo cual no ocurre en ninguna otra plataforma.
Sobre la variable price, la verdad es que el gráfico no permite distinguir bien un comportamiento, por lo que se visualizará de una manera distinta.
fig = make_subplots(rows=5, cols=1)
fig.append_trace(go.Histogram(
x = df.loc[df['rating'] == 'Negative', 'price'],
name = 'Negative'
), row=1, col=1)
fig.append_trace(go.Histogram(
x = df.loc[df['rating'] == 'Mixed', 'price'],
name = 'Mixed'
), row=2, col=1)
fig.append_trace(go.Histogram(
x = df.loc[df['rating'] == 'Mostly Positive', 'price'],
name = 'Mostly Positive'
), row=3, col=1)
fig.append_trace(go.Histogram(
x = df.loc[df['rating'] == 'Positive', 'price'],
name = 'Positive'
), row=4, col=1)
fig.append_trace(go.Histogram(
x = df.loc[df['rating'] == 'Very Positive', 'price'],
name = 'Very Positive'
), row=5, col=1)
fig.update_layout(height=600,
width=1000,
title_text="Price vs rating")
fig.show()
Ahora sí queda más claro cómo distribuye el price en función de su rating. Sólo destacar que en general los juegos con el rating de Negative, poseen un mayor concentración de sus precios hacia valores más bajos. Las demás categorías del rating poseen distribuciones bien similares.
Para la variable required_age, al igual que para english, a simple vista no se nota algún patrón característico, sin embargo, si vemos cómo se comportan las proporciones para cada una de las categorías del rating podemos observar lo siguiente.
fig = make_subplots(rows=5, cols=1)
fig.append_trace(go.Histogram(
x = df.loc[df['rating'] == 'Negative', 'required_age'],
name = 'Negative'
), row=1, col=1)
fig.append_trace(go.Histogram(
x = df.loc[df['rating'] == 'Mixed', 'required_age'],
name = 'Mixed'
), row=2, col=1)
fig.append_trace(go.Histogram(
x = df.loc[df['rating'] == 'Mostly Positive', 'required_age'],
name = 'Mostly Positive'
), row=3, col=1)
fig.append_trace(go.Histogram(
x = df.loc[df['rating'] == 'Positive', 'required_age'],
name = 'Positive'
), row=4, col=1)
fig.append_trace(go.Histogram(
x = df.loc[df['rating'] == 'Very Positive', 'required_age'],
name = 'Very Positive'
), row=5, col=1)
fig.update_layout(height=600,
width=1000,
title_text="Price vs required_age")
fig.show()
En este gráfico se visibiliza de mejor manera como se relaciona la variable required_age con rating, y es posible notar que no existen grandes diferencias en cómo distribuyen las distintas categorías del rating respecto a la edad requerida.
Por último, sobre las variables achievements y average_playtime no es posible observar mucho debido a los valores extremos que posee, por lo que haremos un zoom sobre estos gráficos, filtrando los valores extremos.
px.histogram(data_frame = df.loc[df['achievements'] <= np.percentile(df['achievements'], 95),],
x = 'achievements',
color = 'rating',
barmode = 'overlay',
marginal = 'box')
Es posible notar respecto a la variable achievements que en general para las distintas categorías no existe una gran diferencia en cómo distribuye respecto a ellas. Sí resaltar que para el caso de la categoría negative, la mediana de achievements está mucho más cerca del cero que las medianas de las otras categorías.
px.histogram(data_frame = df.loc[df['average_playtime'] <= np.percentile(df['average_playtime'], 90),],
x = 'average_playtime',
color = 'rating',
barmode = 'overlay',
marginal = 'box')
Por otro lado, sobre la variable average_playtime es posible observar que en general sus distribuciones para las distintas categorías del rating no varían mucho, dejando entrever la poca relación que existe entra estas variables. A diferencia de la variable achievements, para la categoría negative esta variable posee su mediana más lejos del cero respecto de las demás categorías del rating.
Por último, se visualizará como se relaciona la variable release_date respecto al rating, pero para esto primero se creará un nuevo atributo llamado Año debido a que como el rango de fechas que maneja esta variable es muy amplio, se hace dificil poder visualizar cómo distribuye a lo largo del tiempo.
#Creación atributo año
df['Año'] = pd.to_datetime(df['release_date']).dt.year
#gráfico
fig_dims = (15, 8)
fig, ax = plt.subplots(figsize=fig_dims)
sns.histplot(data = df,
x = 'Año',
hue = 'rating', multiple = 'dodge', ax = ax, binwidth = 0.5)
<AxesSubplot:xlabel='Año', ylabel='Count'>
La verdad es que el gráfico demuestra que no existe un patrón claro respecto al rating del juego y el año en que fue lanzado, ya que para cada año las columnas del histograma poseen proporciones muy parecidas sobre las distintas categorías del rating. Por último, se visualizará como se relacionan entre pares de variable numéricas y nuestra variable objetivo rating.
sns.pairplot(data = df,
hue = 'rating')
<seaborn.axisgrid.PairGrid at 0x15fa37a1400>
La verdad es que esta visualización no releva patrones claros sobre relaciones entre variables numéricas y la variable objetivo. En parte se debe a que las distribuciones de la mayoría de las variables numéricas son muy asimétricas y con cólas muy largas. Para entender mejor si existe una relación entre las variables numéricas del dataset, podemos ver la matriz de correlación.
plt.figure(figsize=(12,9))
corr_matrix = df.corr(method = 'spearman')
mask = np.triu(np.ones_like(corr_matrix, dtype=bool))
sns.heatmap(corr_matrix,mask=mask,annot=True)
<AxesSubplot:>
En general se observan valores de correlaciones muy bajos, donde el mayor en valor absoluto es 0.57, entre las variables estimated_sells y average_playtime. En general los valores obtenidos indican que no existen relaciones fuertes entre las variables numéricas.
Ahora haremos el mismo análisis que acabamos de realizar, pero se buscarán las relaciones entre las variables predictoras y la variable estimated_sells que es nuestra variable objetivo para el problema de regresión. Primero recordaremos cómo distribuye esta variable.
df['estimated_sells'].hist()
<AxesSubplot:>
Se observa que la mayoría de sus valores están concentrados bajo los 10 millones. Ahora se visualizará considerando sólo los datos hasta el percentil 95.
df.loc[df['estimated_sells'] < np.percentile(df['estimated_sells'], 95), 'estimated_sells'].hist()
<AxesSubplot:>
Sigue siguendo una distribución muy concentrada en valores muy bajos. Miraremos cómo se comportan con los valores que están bajo los 100000, que equivale al 80% de los datos.
df.loc[df['estimated_sells'] < 100000, 'estimated_sells'].hist()
<AxesSubplot:>
Es posible notar el mismo comportamiento. Por lo tanto se observa que en general la variable estimated_sells está muy concentrada en valores pequeños. A continuación revisaremos como se comportan las demás variables respecto a nuestra variable objetivo. Primero se visualizará cómo se comportan las variables numéricas respecto a estimated_sells. Sólo para poder entender mejor el comportamiento, se graficarán los registros que estén bajo el percentil 95.
#Creación dataset con datos numéricos filtrados por el percentil 95
columnas95 = df.drop(columns = ['english']).select_dtypes(include = ['int64', 'float64']).columns.tolist()
df95 = df.copy()
for columna in columnas95:
df95 = df95.loc[df95[columna] <= np.percentile(df95[columna], 95),]
#Gráfico
fig, axes = plt.subplots(3,2, figsize = (20,24))
atributos = list(df.columns.difference(['name', 'rating', 'estimated_sells',
'rating', 'release_date',
'developer', 'publisher',
'categories', 'genres',
'tags', 'short_description', 'Año-Mes']))
for i in range(3):
for j in range(2):
indice = 2*i + j
if indice <= (len(atributos) - 1):
if df95[atributos[indice]].dtypes in ['int64', 'float64']:
sns.scatterplot(data = df95,
x = 'estimated_sells',
y = atributos[indice],
ax = axes[i][j])
axes[i][j].set(xlabel= 'estimated_sells', ylabel=atributos[indice])
axes[i][j].set_title(atributos[indice])
else:
sns.histplot(data = df95,
x = 'estimated_sells',
hue = atributos[indice],
ax = axes[i][j])
axes[i][j].set(xlabel= 'estimated_sells', ylabel='Frecuencia')
axes[i][j].set_title('estimated_sells por categoría')
fig.tight_layout()
Observando la visualización recién presentada, es posible notar que no existen relaciones claras entre la variable estimated_sells y las demás variables predictoras. Con la variable que más pareciera tener algún grado de complicidad es con average_playtime que es justamente con la que posee mayor correlación según vimos anteriormente. Ahondaremos sobre la variable platforms, debido a que no se logra apreciar bien las distribuciones para cada una de las categorías de ésta.
fig = make_subplots(rows=4, cols=1)
fig.append_trace(go.Histogram(
x = df95.loc[df95['platforms'] == 'windows', 'estimated_sells'],
name = 'windows', nbinsx = 80
), row=1, col=1)
fig.append_trace(go.Histogram(
x = df95.loc[df95['platforms'] == 'windows;mac;linux', 'estimated_sells'],
name = 'windows;mac;linux', nbinsx = 80
), row=2, col=1)
fig.append_trace(go.Histogram(
x = df95.loc[df95['platforms'] == 'windows;mac', 'estimated_sells'],
name = 'windows;mac', nbinsx = 80
), row=3, col=1)
fig.append_trace(go.Histogram(
x = df95.loc[df95['platforms'] == 'windows;linux', 'estimated_sells'],
name = 'windows;linux', nbinsx = 80
), row=4, col=1)
fig.update_layout(height=600,
width=1000,
title_text="Platforms vs estimated_sells")
fig.show()
En este gráfico sí se logra entender que en general las distribuciones de estimated_sells para las distintas categorías de platforms se comportan de manera muy similar, lo que da a entender de que saber la plataforma no entre mucha información sobre las ventas estimadas. A continuación veremos cómo se comportan las medianas de las ventas estimadas de los juegos a lo largo de los años.
df.groupby(by = ['Año']).agg({'estimated_sells': 'median'}).plot()
<AxesSubplot:xlabel='Año'>
Interesante notar que hay dos picks bien altos durante el periodo de estudio, en el año 1998 y 2004. La verdad es que intuitivamente el año de creación de un juego no tendría que por qué determinar su estimated_sells, ni su rating.
Ahora realizaremos una reducción de dimensionalidad de nuestro dataset, considerando sólo las variables numéricas, y proyectaremos las dos componentes principales, para ver si existe alguna relación entre éstas y nuestras variables objetivo.
umap = UMAP(random_state=55)
projections = umap.fit_transform(df.select_dtypes(include = ['int64', 'float64']))
df_proj = pd.DataFrame(projections, columns=["primeraComponente", "segundaComponente"])
df_fig = df.copy()
df_fig = pd.concat([df_fig, df_proj], axis=1)
fig = px.scatter(
df_fig,
x="primeraComponente",
y="segundaComponente",
color='rating',
template="plotly_dark",
opacity = 0.5,
)
fig.show()
fig = px.scatter(
df_fig,
x="primeraComponente",
y="segundaComponente",
color='estimated_sells',
template="plotly_dark",
opacity = 0.5
)
fig.show()
Viendo las visualizaciones anteriores, la verdad es que en la primera, no se logra diferenciar bien algún patrón debido a la superposición de los puntos, sin embargo si aprovechamos la interactividad de plotly y vamos seleccionando una por una las categorías del rating, entonces podemos darnos cuenta de que no hay una separación cuando se utilizan las dos componentes para proyectar las variables numéricas. Sobre la segunda visualización, podemos observar que los valores grandes de estimated_sells están concentrados entre los intervalos [-10, -5] del eje y y [-10, -5] del eje x. Para este último pareciera ser que UMAP encuentra algunas diferencia que le permiten distinguir los registros por sus ventas estimadas.
A continuación abordaremos las variables predictoras que no abordamos anteriormente, developer, publisher, categories, genres, tags y short_description, buscando relaciones respecto las variables predictoras. Se partirá observando su relación con la variable rating, comenzando por la variable developer.
df['developer'].value_counts().value_counts()
1 4227 2 644 3 217 4 103 5 60 6 37 7 29 8 10 9 9 15 7 10 5 13 4 14 4 11 2 12 2 17 2 23 1 19 1 32 1 Name: developer, dtype: int64
Lo primero que podemos observar es que en general la mayoría de las empresas desarrolladoras poseen sólo 1 juego, por lo menos en los registros de nuestro dataset. Transformaremos esta variable a numérica, reemplazándola por la cantidad de juegos que posee.
mapDeveloper = dict(zip(df['developer'].value_counts().index,
df['developer'].value_counts()))
dfTransformado = df.copy()
dfTransformado['developer'] = dfTransformado['developer'].map(mapDeveloper)
fig = make_subplots(rows=5, cols=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Negative', 'developer'],
name = 'Negative', nbinsx = 80
), row=1, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Mixed', 'developer'],
name = 'Mixed', nbinsx = 80
), row=2, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Mostly Positive', 'developer'],
name = 'Mostly Positive', nbinsx = 80
), row=3, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Positive', 'developer'],
name = 'Positive', nbinsx = 80
), row=4, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Very Positive', 'developer'],
name = 'Very Positive', nbinsx = 80
), row=5, col=1)
fig.update_layout(height=600,
width=1000,
title_text="developer vs rating")
fig.show()
El gráfico muestra que la cantidad de juegos que ha hecho la compañía desarrolladora no está relacionada con el rating del juego, ya que para las distintas categorías del rating, la variable developer (transformada) distribuye de la misma manera. A continuación haremos el mismo ejercicio pero con la variable publisher.
mapPublisher = dict(zip(df['publisher'].value_counts().index,
df['publisher'].value_counts()))
dfTransformado['publisher'] = dfTransformado['publisher'].map(mapPublisher)
fig = make_subplots(rows=5, cols=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Negative', 'publisher'],
name = 'Negative', nbinsx = 80
), row=1, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Mixed', 'publisher'],
name = 'Mixed', nbinsx = 80
), row=2, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Mostly Positive', 'publisher'],
name = 'Mostly Positive', nbinsx = 80
), row=3, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Positive', 'publisher'],
name = 'Positive', nbinsx = 80
), row=4, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Very Positive', 'publisher'],
name = 'Very Positive', nbinsx = 80
), row=5, col=1)
fig.update_layout(height=600,
width=1000,
title_text="publisher vs rating")
fig.show()
Nuevamente nos encontrarmos con que no existe mucha diferencia entre cómo distribuye la variable transformada de publisher con las distintas categorías del rating. A continuación observaremos cómo se relaciona la variable categories con el rating del juego. Lo primero notar que este atributo contiene varias categorías permitidas para ese juego. Lo que se realizará será separar estas categorías y ver si por sí sólas se relacionan o no con el rating.
#se desagregan las categorías de cada juego.
categoriasSeparadas = df['categories'].str.split(';').apply(pd.Series).stack()
#se crea un nuevo dataframe para manejar este atributo de manera especial
dfTransformado2 = (df.reset_index()
.merge(categoriasSeparadas.reset_index(),
how = 'left',
left_on = 'index',
right_on = 'level_0')
.rename(columns = {0:'Categoria'}))
px.bar(data_frame=(dfTransformado2
.groupby(by = ['Categoria', 'rating'])['name']
.count()
.reset_index()
.rename(columns = {'name': 'Cantidad'})),
x="Categoria",
y="Cantidad",
color="rating",
barmode="group")
En esta visualización no se aprecian patrones muy claros, sí mencionar que para la categoría MMO llama la atención que, en proporción, posea más votos negativos y mixtos. Ahora analizaremos esta variable desde la perspectiva de cuántas categorías admite un juego en particular.
dfTransformado['numCategories'] = df['categories'].str.split(';').apply(lambda x: len(x))
fig = make_subplots(rows=5, cols=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Negative', 'numCategories'],
name = 'Negative'
), row=1, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Mixed', 'numCategories'],
name = 'Mixed'
), row=2, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Mostly Positive', 'numCategories'],
name = 'Mostly Positive'
), row=3, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Positive', 'numCategories'],
name = 'Positive'
), row=4, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Very Positive', 'numCategories'],
name = 'Very Positive'
), row=5, col=1)
fig.update_layout(height=600,
width=1000,
title_text="Cantidad de categorías vs rating")
fig.show()
Nuevamente no se observan relaciones claras entre las categorías del rating y la cantidad de categories que tiene un juego. A continuación pasaremos a revisar la variable predictora genres, y se abordará de la misma manera que se abordó la variable anterior, debido a que también contiene múltiples generos dentro del mismo atributo.
#se desagregan los géneros de cada juego.
generosSeparados = df['genres'].str.split(';').apply(pd.Series).stack()
dfTransformado3 = (df.reset_index()
.merge(generosSeparados.reset_index(),
how = 'left',
left_on = 'index',
right_on = 'level_0')
.rename(columns = {0:'Genero'}))
px.bar(data_frame=(dfTransformado3
.groupby(by = ['Genero', 'rating'])['name']
.count()
.reset_index()
.rename(columns = {'name': 'Cantidad'})),
x="Genero",
y="Cantidad",
color="rating",
barmode="group")
Notar que para los generos Early Access, Free to Play, Gore, Massively Multiplayer, Simulation y Violent poseen más votos Negativos y Mixtos (dentro de tu propio género) en proporción a los demás tipos de rating. Ahora observaremos si la cantidad de géneros que tiene un juego tiene relación con su rating.
dfTransformado['numGenres'] = df['genres'].str.split(';').apply(lambda x: len(x))
fig = make_subplots(rows=5, cols=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Negative', 'numGenres'],
name = 'Negative'
), row=1, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Mixed', 'numGenres'],
name = 'Mixed'
), row=2, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Mostly Positive', 'numGenres'],
name = 'Mostly Positive'
), row=3, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Positive', 'numGenres'],
name = 'Positive'
), row=4, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Very Positive', 'numGenres'],
name = 'Very Positive'
), row=5, col=1)
fig.update_layout(height=600,
width=1000,
title_text="Cantidad de géneros vs rating")
fig.show()
Viendo el gráfico anterior, no se observan distribuciones diferentes para las cantidad de géneros respecto a las distintas categorías del rating. A continuación, observaremos la variable tags la cual es muy parecida a genres, por lo que se abordará de la misma manera. Sólo porque existen más de 300 tags únicos, se eligirán los que estén en más de 15 juegos para poder realizar la visualización.
#se desagregan los géneros de cada juego.
tagsSeparados = df['tags'].str.split(';').apply(pd.Series).stack()
dfTransformado4 = (df.reset_index()
.merge(tagsSeparados.reset_index(),
how = 'left',
left_on = 'index',
right_on = 'level_0')
.rename(columns = {0:'Tag'}))
dfCategorias = (dfTransformado4
.groupby(by = ['Tag', 'rating'])['name']
.count()
.reset_index()
.rename(columns = {'name': 'Cantidad'}))
px.bar(data_frame=dfCategorias.loc[dfCategorias['Cantidad'] > 15],
x="Tag",
y="Cantidad",
color="rating",
facet_row='rating',
barmode="group", height = 1000, width = 1000)
Es posible observar que en general no hay una tendencia clara a que ciertos tags sean evaluados con ratings distintos. Ahora observaremos si la cantidad de tags que posee un juego influye en el rating.
dfTransformado['numTags'] = df['tags'].str.split(';').apply(lambda x: len(x))
fig = make_subplots(rows=5, cols=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Negative', 'numTags'],
name = 'Negative', nbinsx = 5
), row=1, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Mixed', 'numTags'],
name = 'Mixed', nbinsx = 5
), row=2, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Mostly Positive', 'numTags'],
name = 'Mostly Positive', nbinsx = 5
), row=3, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Positive', 'numTags'],
name = 'Positive', nbinsx = 5
), row=4, col=1)
fig.append_trace(go.Histogram(
x = dfTransformado.loc[dfTransformado['rating'] == 'Very Positive', 'numTags'],
name = 'Very Positive', nbinsx = 5
), row=5, col=1)
fig.update_layout(height=600,
width=1000,
title_text="Cantidad de tags vs rating")
fig.show()
Es posible observar que la mayoría de los juegos poseen 3 tags, sin hacer distinción sobre su rating. Por último, sobre la variable short_description, no la consideraremos para la clasificación por lo que se visualizará. La decisión de no utilizarla es porque según juicio experto la descripción de un juego no influye ni en su rating ni en sus estimated_sells.
En esta sección realizaremos todas las transformaciones necesarias a nuestros datos iniciales para posteriormente realizar nuestra predicción. Se crean dos funciones de preprocesamiento, la primera para el problema de clasificación y la segunda para el problema de regresión. En estas funciones se realizan las transformaciones a las variables categóricas, como también a algunas variables numéricas, las cuales son explicada dentro de éstas.
def preprocesarClasificacion(data):
df = data.copy()
#Se codifica el required_age como una variable binaria
df['required_age'] = (df['required_age'] != 0).map({True: 1, False: 0})
#Se cambia el release_date por el año
df['Año'] = pd.to_datetime(df['release_date']).dt.year
#Las variables developer y publisher se transformarán a la cantidad de juegos en las que han pertenecido
mapDeveloper = dict(zip(df['developer'].value_counts().index,
df['developer'].value_counts()))
mapPublisher = dict(zip(df['publisher'].value_counts().index,
df['publisher'].value_counts()))
df['developer'] = df['developer'].map(mapDeveloper)
df['publisher'] = df['publisher'].map(mapPublisher)
#La variable platforms la dividiremos y se transformará en 3 columnas binarias en donde en cada una de ellas se
#preguntará si funciona en cierto sistema operativo o no.
df['windows'] = df['platforms'].str.split(';').apply(lambda x: 'windows' in x).map({True: 1, False: 0})
df['mac'] = df['platforms'].str.split(';').apply(lambda x: 'mac' in x).map({True: 1, False: 0})
df['linux'] = df['platforms'].str.split(';').apply(lambda x: 'linux' in x).map({True: 1, False: 0})
#Los atributos categories, genres y tags se transformarán a variables numéricas indicando la cantidad de categorías
#que posee cada atributo
df['#categories'] = df['categories'].str.split(';').apply(lambda x: len(x))
df['#genres'] = df['genres'].str.split(';').apply(lambda x: len(x))
df['#tags'] = df['tags'].str.split(';').apply(lambda x: len(x))
#También se creará una nueva columna que contenga todas las categorías, géneros y Tags, para luego
#realizar un bag of words
df['tagsGenresCategories'] = (df['tags'] + ';' + df['genres'] + ';' + df['categories'])
#Eliminar columnas que no se utilizarán para la predicción.
df.drop(columns = ['name', 'Año-Mes', 'short_description', 'release_date',
'platforms', 'categories', 'genres', 'tags', 'estimated_sells'],
inplace = True,
errors = 'ignore')
#Se retorna el dataframe preprocesado
return df
def preprocesarRegresion(data):
df = data.copy()
#Se codifica el required_age como una variable binaria
df['required_age'] = (df['required_age'] != 0).map({True: 1, False: 0})
#Se cambia el release_date por el año
df['Año'] = pd.to_datetime(df['release_date']).dt.year
#Las variables developer y publisher se transformarán a la cantidad de juegos en las que han pertenecido
mapDeveloper = dict(zip(df['developer'].value_counts().index,
df['developer'].value_counts()))
mapPublisher = dict(zip(df['publisher'].value_counts().index,
df['publisher'].value_counts()))
df['developer'] = df['developer'].map(mapDeveloper)
df['publisher'] = df['publisher'].map(mapPublisher)
#La variable platforms la dividiremos y se transformará en 3 columnas binarias en donde en cada una de ellas se
#preguntará si funciona en cierto sistema operativo o no.
df['windows'] = df['platforms'].str.split(';').apply(lambda x: 'windows' in x).map({True: 1, False: 0})
df['mac'] = df['platforms'].str.split(';').apply(lambda x: 'mac' in x).map({True: 1, False: 0})
df['linux'] = df['platforms'].str.split(';').apply(lambda x: 'linux' in x).map({True: 1, False: 0})
#Los atributos categories, genres y tags se transformarán a variables numéricas indicando la cantidad de categorías
#que posee cada atributo
df['#categories'] = df['categories'].str.split(';').apply(lambda x: len(x))
df['#genres'] = df['genres'].str.split(';').apply(lambda x: len(x))
df['#tags'] = df['tags'].str.split(';').apply(lambda x: len(x))
#También se creará una nueva columna que contenga todas las categorías, géneros y Tags, para luego
#realizar un bag of words
df['tagsGenresCategories'] = (df['tags'] + ';' + df['genres'] + ';' + df['categories'])
#Eliminar columnas que no se utilizarán para la predicción.
df.drop(columns = ['name', 'Año-Mes', 'short_description', 'release_date',
'platforms', 'categories', 'genres', 'tags', 'rating'],
inplace = True,
errors = 'ignore')
#Se retorna el dataframe preprocesado
return df
Luego se crea un objeto CountVectorizer que se encargará de identificar qué género, categoría y tag está dentro del juego, creando una columna para cada posible valor de los géneros, categorías y tags.
import re
re_exp = r"\;"
stop_words = stopwords.words('english')
# Definimos un tokenizador con Stemming
class StemmerTokenizer:
def __init__(self):
self.ps = PorterStemmer()
def __call__(self, doc):
doc_tok = word_tokenize(doc)
doc_tok = [t for t in doc_tok if t not in stop_words]
return [self.ps.stem(t) for t in doc_tok]
bog = CountVectorizer(tokenizer=lambda text: re.split(re_exp,text))
# bog = CountVectorizer(tokenizer=StemmerTokenizer())
# StemmerTokenizer()
A continuación se definen las columnas numéricas a las cuales se les aplicarán los distintos tipos de escalamientos.
#Columnas a las que se le aplicará un RobustScaler
# columnasAEscalar = ['achievements', 'average_playtime', 'price', 'developer',
# 'publisher', '#categories', '#genres', '#tags', 'Año']
columnasAEscalar = preprocesarClasificacion(df).select_dtypes(include = ['int64', 'float64']).columns
Posteriormente se definen 3 transformaciones, cuyas diferencias radican en el tipo de escalamiento que se realiza sobre las variables numéricas.
#Transformación de columnas según su tipo.
transConBOG = ColumnTransformer(transformers =
[
('Escalamiento', RobustScaler(), columnasAEscalar),
('Bog', bog, 'tagsGenresCategories')
],
remainder = 'drop',
verbose_feature_names_out=False)
#Transformación de columnas según su tipo.
transSinBOG = ColumnTransformer(transformers =
[
('Escalamiento', RobustScaler(), columnasAEscalar),
],
remainder = 'drop',
verbose_feature_names_out=False)
A continuación se pruebas las transformaciones realizadas de manera de corroborar que no existan problemas.
transConBOG.fit_transform(preprocesarClasificacion(df))
<7881x346 sparse matrix of type '<class 'numpy.float64'>' with 139959 stored elements in Compressed Sparse Row format>
transSinBOG.fit_transform(preprocesarClasificacion(df))
array([[ 0. , 0. , -0.09090909, ..., -0.5 ,
1. , 0. ],
[ 0. , 0.5 , 0.09090909, ..., 0.25 ,
0.5 , 0. ],
[ 0. , 7. , 3.09090909, ..., 0. ,
-0.5 , 0. ],
...,
[ 0. , 0. , -0.09090909, ..., 0.25 ,
-0.5 , 0. ],
[ 0. , 0. , -0.09090909, ..., 0.5 ,
0. , 0. ],
[ 0. , 1. , 0.09090909, ..., 0.25 ,
-0.5 , 0. ]])
Se puede observar que las transformaciones se realizan sin mayores incovenientes.
En esta sección se creará un modelo baseline el cual servirá como base de comparación para los próximos modelos que se implementen. Primero se crea una función que nos permita entrenar y evaluar el modelo.
#función que permite entrenar y mostrar el desempeño del modelo
def train_and_evaluate(pipe,
X_train,
y_train,
X_test,
y_test,
tipo,
print_=True):
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
if print_:
if tipo == 'clas_multiclase':
print('Matriz de confusión: \n')
print(confusion_matrix(y_test, y_pred, labels=pipe.classes_))
print('\nReporte de Clasificación: \n')
print(classification_report(y_test, y_pred, labels=pipe.classes_))
else:
print('R2: \n')
print(r2_score(y_test, y_pred))
return
Adjuntamos las transformaciones anteriores en un solo Pipeline y agregamos al final un clasificador sencillo, que será el KNN para el caso de la clasificación y una regresión lineal para el caso de la regresión. Primero se realiza para la clasificación y luego para la regresión.
pipeKNN = Pipeline([
('Procesamiento', FunctionTransformer(preprocesarClasificacion)),
('Transformacion', transConBOG),
('Clasificador', KNeighborsClassifier())
])
#Separación de conjunto de datos
features = df.drop(columns = ['rating'])
labels = df['rating']
X_train, X_test, y_train, y_test = train_test_split(
features, labels, test_size=0.33, shuffle=True, stratify=labels, random_state = 100
)
train_and_evaluate(pipeKNN,
X_train,
y_train,
X_test,
y_test,
'clas_multiclase')
Matriz de confusión:
[[190 136 110 89 22]
[142 145 106 132 38]
[147 63 133 70 13]
[162 174 75 184 75]
[ 73 87 26 128 81]]
Reporte de Clasificación:
precision recall f1-score support
Mixed 0.27 0.35 0.30 547
Mostly Positive 0.24 0.26 0.25 563
Negative 0.30 0.31 0.30 426
Positive 0.31 0.27 0.29 670
Very Positive 0.35 0.21 0.26 395
accuracy 0.28 2601
macro avg 0.29 0.28 0.28 2601
weighted avg 0.29 0.28 0.28 2601
Se observa que para el caso de la clasificación el modelo no obtiene muy buenos resultados, en donde la clase que en la que posee mejor desempeño respecto al f1-score es la Mixed y la Negative, con un de 0.30 (este valor puede cambiar al volver a ejecutar las celdas). Ahora veremos cómo se comporta para la regresión, para este problema utilizaremos un árbol de decisión.
#Separación de conjunto de datos
features1 = df.drop(columns = ['estimated_sells'])
labels1 = df['estimated_sells']
X_train1, X_test1, y_train1, y_test1 = train_test_split(
features1, labels1, test_size=0.33, shuffle=True, random_state = 105
)
#creación pipe de regresión
pipeTree= Pipeline([
('Procesamiento', FunctionTransformer(preprocesarRegresion)),
('Transformacion', transConBOG),
('Clasificador', DecisionTreeRegressor())
])
#entrenamiento y evaluación
train_and_evaluate(pipeTree,
X_train1,
y_train1,
X_test1,
y_test1,
'regresion')
R2: -0.02794900420767399
Se puede observar que el modelo de árbol de decisión nos entrega un r2 negativo, lo que indica que en el fondo nos entrega una peor predicción que haber predecido símplemente el promedio de nuestra variable estimated_sells. Luego de tener estos baseline, procederemos a buscar otros modelos y optimizarlos para mejorar nuestro desempeño.
En este apartado buscaremos mejorar los resultados obtenidos en el baseline, probando distintos modelos y parámetros de éstos.
#Clase auxiliar que nos permite transformar una matriz sparse a una matriz normal
from sklearn.base import BaseEstimator, TransformerMixin
class toarray_class(BaseEstimator, TransformerMixin):
def __init__(self, sparse_matrix = True):
self.sparse_matrix = sparse_matrix
def fit(self, X, y = None):
return self
def transform(self, X, y = None):
if type(X) == np.ndarray:
return X
else:
return X.toarray()
Para el problema de clasificación, probaremos los modelos GradientBoostingClassifier y HistGradientBoostingClassifier, ambos son modelos ensamblados. Se creará un pipeline con los pasos de transformación de datos, en donde se aplicarán las transformaciones creadas con los métodos preprocesarClasificacion y preprocesarRegresion, luego el de preprocesamiento en donde se aplicarán las transformaciones establecidas con el column transformer, posteriormente se implementa la selección de atributos basado en el SelectionPercentile que viene precedido por la previa trasformación de la matriz sparte a un array, y por último se implementa el clasificador. Para los clasificadores utilizados, se variarán algunos de sus hiperparámetros para buscar el modelo óptimo.
#pipe para la búsqueda de grilla
pipelineSinPCA = Pipeline(steps=[
("Transformaciones", FunctionTransformer(preprocesarClasificacion)),
("Preprocesamiento", transConBOG),
("to_array", toarray_class()),
("Selection", SelectPercentile()),
('clf', GradientBoostingClassifier())
])
#grilla de hiperparámetros
param_grid = [
{
"Preprocesamiento": [transConBOG,
transSinBOG
],
"Preprocesamiento__Escalamiento": [RobustScaler(), StandardScaler(), MinMaxScaler()],
"Selection__percentile": [20,40,60,80,100],
"Selection__score_func": [f_classif, mutual_info_classif],
'clf': [GradientBoostingClassifier(random_state = 102)],
'clf__n_estimators': [100,500,1000,2000],
'clf__max_depth': [4, 8,15]},
{
"Preprocesamiento": [transConBOG, transSinBOG],
"Preprocesamiento__Escalamiento": [RobustScaler(), StandardScaler(), MinMaxScaler()],
"Selection__percentile": [20,40,60,80,100],
"Selection__score_func": [f_classif, mutual_info_classif],
'clf': [HistGradientBoostingClassifier(random_state = 102)],
'clf__max_iter': [100,500,1000,2000],
'clf__learning_rate': [0.1, 0.3, 0.5, 1]}
]
#Creación y búsqueda de hiperparámetros
hgs = HalvingGridSearchCV(pipelineSinPCA, param_grid, n_jobs = -1, verbose = 10, scoring = 'f1_weighted')
train_and_evaluate(hgs, X_train, y_train, X_test, y_test, tipo = 'clas_multiclase')
n_iterations: 5 n_required_iterations: 7 n_possible_iterations: 5 min_resources_: 50 max_resources_: 5280 aggressive_elimination: False factor: 3 ---------- iter: 0 n_candidates: 1680 n_resources: 50 Fitting 5 folds for each of 1680 candidates, totalling 8400 fits ---------- iter: 1 n_candidates: 560 n_resources: 150 Fitting 5 folds for each of 560 candidates, totalling 2800 fits ---------- iter: 2 n_candidates: 187 n_resources: 450 Fitting 5 folds for each of 187 candidates, totalling 935 fits ---------- iter: 3 n_candidates: 63 n_resources: 1350 Fitting 5 folds for each of 63 candidates, totalling 315 fits
Los resultados muestran que el modelo en promedio mejoró sus resultados tanto macros como weighted para todas las métricas de desempeños. Sin embargo, si observamos las categorías individuales, podemos notar que por ejemplo, para la métrica de recall, para las clases Mixed y Mostly Positive disminuyó su desempeño en 7 y 1 punto respectivamente respecto del baseline. Sobre el f1-score notamos que el baseline es superior sólo en la clases Mixed, determinado por la mejora en el recall. A continuación observaremos cual fue la mejor combinación de parámetros que dieron los resultados recién presentados.
hgs.best_params_
{'Preprocesamiento': ColumnTransformer(transformers=[('Escalamiento', StandardScaler(),
Index(['english', 'developer', 'publisher', 'required_age', 'achievements',
'average_playtime', 'price', 'Año', 'windows', 'mac', 'linux',
'#categories', '#genres', '#tags'],
dtype='object')),
('Bog',
CountVectorizer(tokenizer=<function <lambda> at 0x000002084086BD30>),
'tagsGenresCategories')],
verbose_feature_names_out=False),
'Preprocesamiento__Escalamiento': StandardScaler(),
'Selection__percentile': 100,
'Selection__score_func': <function sklearn.feature_selection._univariate_selection.f_classif(X, y)>,
'clf': GradientBoostingClassifier(max_depth=8, random_state=102),
'clf__max_depth': 8,
'clf__n_estimators': 100}
Se puede observar que para el step de Preprocesamiento el escalamiento utilizado fue el StandardScaler en conjunto con la utilización de Bag of Words para las categorías, géneros y tags. Luego en el paso de selección, se utilizaron el 100% de los atributos en conjunto con f_classif para la función de score. Por último, el clasificador utilizado fue el GradientBoosting con un max_depth de 8 y 100 estimadores. A continuación se optimizará el modelo del problema de regresión para mejorar los resultados obtenidos en el baseline. Se utilizarán los mismos modelos usados en el problema de clasificación, pero ahora para resolver el problema de regresión. El pipeline que se utilizará tendrá los mismos pasos, sin embargo cambia la función utilizada en el paso de transformaciones, en donde se implementará la creada para la regresión, y también en el paso de selección se cambia el SelectionPercentile por una reducción de dimensionalidad a través del PCA.
#Pipe problema de regresión
pipelineConPCARegresion = Pipeline(steps=[
("Transformaciones", FunctionTransformer(preprocesarRegresion)),
("Preprocesamiento", transConBOG),
("to_array", toarray_class()),
("Selection", PCA(n_components = 20)),
('clf', GradientBoostingRegressor())
])
#grilla de hiperparámetros
param_gridRegresion = [
{
"Preprocesamiento": [transConBOG, transSinBOG],
"Preprocesamiento__Escalamiento": [RobustScaler(), StandardScaler(), MinMaxScaler()],
"Selection__n_components": list(range(1,15)),
'clf': [GradientBoostingRegressor(random_state = 102)],
'clf__n_estimators': [100,500,1000,2000],
'clf__max_depth': list(range(1,15))},
{
"Preprocesamiento": [transConBOG, transSinBOG],
"Preprocesamiento__Escalamiento": [RobustScaler(), StandardScaler(), MinMaxScaler()],
"Selection__n_components": list(range(1,15)),
'clf': [HistGradientBoostingRegressor(random_state = 102)],
'clf__max_iter': [100,500,1000,2000],
'clf__learning_rate': list(np.arange(0.1,1, 0.1))}
]
#Creación y búsqueda de hiperparámetros
hgs1 = HalvingGridSearchCV(pipelineConPCARegresion,
param_gridRegresion,
n_jobs = -1,
verbose = 10,
min_resources = 50,
scoring = 'r2',
error_score='raise')
train_and_evaluate(hgs1, X_train1, y_train1, X_test1, y_test1, tipo = 'regresion')
hgs1.best_params_
{'Preprocesamiento': ColumnTransformer(transformers=[('Escalamiento', StandardScaler(),
Index(['english', 'developer', 'publisher', 'required_age', 'achievements',
'average_playtime', 'price', 'Año', 'windows', 'mac', 'linux',
'#categories', '#genres', '#tags'],
dtype='object')),
('Bog',
CountVectorizer(tokenizer=<function <lambda> at 0x000002084086BD30>),
'tagsGenresCategories')],
verbose_feature_names_out=False),
'Preprocesamiento__Escalamiento': MinMaxScaler(),
'Selection__n_components': 4,
'clf': GradientBoostingRegressor(max_depth=1, random_state=102),
'clf__max_depth': 1,
'clf__n_estimators': 100}
Se puede observar que el modelo que alcanzó mejores resultados fue el que utilizó como preprocesamiento el escalamiento de MinMax, en conjunto con el Bag of words, utilzando 4 componentes para el PCA y un GradientBoostingRegressor como clasificador, con jun max_depth de 1 y 100 estimadores.
Luego de haber abordado el problema planteado, podemos observar que si bien los resultados no son muy buenos ni para la predicción del rating ni de las estimated_sells, obteniendo mejores resultados para la primera, se logró superar el baseline, lo cual es un logro. Es pertinente mencionar que los resultados del baseline para la clasificación no distan mucho de los obtenidos al realizar la optimización, lo que refleja que la optimización de hiperparámetros logra incrementar el rendimiento, pero no es que vaya a realizar maravillas sobre el desempeño del modelo, y eso que se utilizó un KNN, un modelo bien sencillo, para el baseline. Los resultados sobre el f1_weighted mejoraron en 5 puntos.
Sobre el regresor la verdad es que no se logró conseguir un buen modelo inclusive luego de la optimización, obteniendo un r2 de apenas 0.016, lo cual indica que el modelo es capaz de explicar sólo el 1.6% de la varianza de las ventas estimadas, lo cual es muy poco.
Sobre los resultados obtenidos, mencionar que no quedé conforme con el desempeño de los modelos ya que si lo necesitamos para tomar una decisión tan delicada como la inversión de dinero, no nos entrega la confianza necesaria para determinar donde debería destinarlo. Sin embargo, creo que dado los datos brindados, los resultados no pueden mejorarse mucho más a menos que se incluyan nuevos atributos, ya que fue posible observar en el análisis exploratorio que no existían patrones claros que nos permitieran diferenciar bien entre las distintas categorías del rating o los valores de las estimated_sells para los distintas características del dataset.
Creo que la competencia permitió generar un insentivo extra para quienes les gusta competir y así tratar de optimizar al máximo el desempeño, probando distintas combinaciones de hiper parámetros por harto tiempo para poder obtener el mejor resultado. Siento que el espíritu de competir nos lleva un paso más allá para conseguir el objetivo, y nos permite explorar diferentes caminos para poder llegar al objetivo.
Por último, mencionar que el proyecto nos permitió interiorizar de mejor manera los contenidos vistos en el curso ya que se abordaron gran parte de los tópicos del ramo de manera integral, pudiendo entender la cadena completa. Esto nos da una visión más acabada de lo que se espera sobre un proyecto de ciencia de datos.